'''
CSV exporter for the BCK animation type
CSV to be used with the j3d animation editor program
'''

import bpy, math, re
from mathutils import Matrix
from .my_functions import *

# Notes (AFAIK):

# - position/rotation/scaling values of a bone in an animation
#   must be the ones that are relative to its parent bone
# - Extrinsic Euler XYZ system is the one being used for rotation values.
# - all animations must start in Frame 0 (starting frame).
    
######################################
# write_csv_bck (MAIN FUNCTION)
# function to write a CSV file with 
# data for the BCK animation type
# to be used with J3D Animation Editor 
######################################
def write_csv_bck(context, filepath, loop_mode, export_mode):
  
  # always needed
  scene = bpy.context.scene
  
  # loop mode variable declaration
  loop_number = 0
  
  if (loop_mode == "OPT_A"):
    loop_number = 0
  elif (loop_mode == "OPT_B"):
    loop_number = 1
  elif (loop_mode == "OPT_C"):
    loop_number = 2
  elif (loop_mode == "OPT_D"):
    loop_number = 3
  elif (loop_mode == "OPT_E"):
    loop_number = 4
  
  # if nothing is selected end the exporter
  if (scene.objects.active == None
      or
      scene.objects.active.type != 'ARMATURE'):
    error_string = "No Armature object selected. Select one and try again."
    print("\n### ERROR ###\n" + error_string + "\n### ERROR ###\n")
    show_message(error_string, "Error exporting collada file", 'ERROR')
    return {'FINISHED'}
  
  # get armature object
  armature = scene.objects.active  
  print()
  print("###############")
  print("Armature found: %s" % armature.name)
  print()
  
  print("Creating CSV file (for BCK)...")
  
  # open file to write
  f = open(filepath, 'w', encoding='utf-8')
  
  print("Writing CSV header...")
  
  ###############################################
  # write the "header" of the CSV animation table
  ###############################################
  
  animation_length = scene.frame_end + 1
  f.write("%d,%d,%d,%s" % (loop_number, animation_length - 1, 0, ".bck"))
  f.write("\n")
  
  f.write("Bone Name,Tangent Interpolation,Component")

  for frame in range(animation_length):
    f.write(",Frame %d" % (frame))
  f.write("\n")
  
  ####################################################
  # get a detailed list of the keyframes used in the 
  # animation and the bones related to those keyframes 
  #
  # bone_kf_data will hold the bone and keyframe position as follows:
  #
  # bone name --> name of the animated bone
  # anim property --> Scale/Rotation/Translation
  # axis --> 0/1/2 --> X/Y/Z
  #
  # a bone name, anim property, axis, kf 1 pos, kf 2 pos, ...
  # another bone name, anim property, axis, kf 3 pos, kf 4 pos, ...
  # ...
  # 
  # Note that each animation property can have different keyframe positions in time  
  
  bone_kf_data = []
      
  for i in range(len(armature.animation_data.action.fcurves)):
  #  
    # get curve
    curve = armature.animation_data.action.fcurves[i]    
    # add new row for given curve in bone_kf_data
    bone_kf_data.append([])
    
    ################################################################
    # append bone name, animation property and axis related to curve
        
    # bone name
    bone_kf_data[i].append(re.search('\["(.+?)"\]', curve.data_path).group(1))
    
    # anim property
    if (curve.data_path.find("scale") + 1):
      bone_kf_data[i].append("scale")
    elif (curve.data_path.find("rotation_euler") + 1):
      bone_kf_data[i].append("rotation")
    elif (curve.data_path.find("location") + 1):
      bone_kf_data[i].append("translation")    
    # axis
    if (curve.array_index == 0):
      bone_kf_data[i].append("x")
    elif (curve.array_index == 1):
      bone_kf_data[i].append("y")
    elif (curve.array_index == 2):
      bone_kf_data[i].append("z")      
    
    # store keyframe data 
    for j in range(len(curve.keyframe_points)):
      keyframe = curve.keyframe_points[j]
      bone_kf_data[i].append(int(keyframe.co[0])) # keyframe pos is an integer (frame)
  #  
  
  # print bone_kf_data to terminal
  print()
  for row in bone_kf_data:
    print(row)
  
  ############################################################
  # get the armature bones that contain 2 or more keyframes
  # defined (read bone_kf_data) and store at the side of
  # each bone name the last keyframe position of its animation
  # 
  # bone_last_kf_pos will contain data as follows:
  #
  # bone1 name, last bone1 keyframe position, bone2 name, last bone2 keyframe position, ...
  #
  bone_last_kf_pos = []  
  for i in range(len(bone_kf_data)):
  #  
    if (len(bone_last_kf_pos) != 0):
      # if the last bone name on bone_last_kf_pos is the same as the
      # one on bone_kf_data[i][0] go to the column element
      if (bone_last_kf_pos[len(bone_last_kf_pos) - 2] == bone_kf_data[i][0]):
        # check if the keyframe position of the bone is larger and store the larger value
        if (bone_last_kf_pos[len(bone_last_kf_pos) - 1] < bone_kf_data[i][len(bone_kf_data[i]) - 1]):
          bone_last_kf_pos[len(bone_last_kf_pos) - 1] = bone_kf_data[i][len(bone_kf_data[i]) - 1]
        continue
    
    # bone animation row has more than 1 keyframe on an anim property
    # append bone name and last keyframe position in time
    if (len(bone_kf_data[i]) > 4):    
      bone_last_kf_pos.append(bone_kf_data[i][0])
      bone_last_kf_pos.append(bone_kf_data[i][len(bone_kf_data[i]) - 1])      
  #
  
  # print bones_with_kf to terminal
  print()
  print(bone_last_kf_pos)
  print()
  
  ###################################################################
  # read animation data for one bone then dump the animation data for 
  # said bone on the CSV file
  ###################################################################
  
  ########################
  # loop through each bone
  for i in range(len(armature.pose.bones)):
  
    # get bone
    bone = armature.pose.bones[i]
    # print bone going to be processed on terminal
    print("Processing animation for bone: %s" % (bone.name))       
    
    # store the animation data scale/rotation/translation X/Y/Z on
    # bone_anim_data which will have 9 rows and each row will be the
    # length of the animation + 1 (including Frame 0 values)
    
    # row 1 --> Scale X
    # row 2 --> Scale Y
    # row 3 --> Scale Z
    # row 4 --> Rotation X
    # row 5 --> Rotation Y
    # row 6 --> Rotation Z
    # row 7 --> Translation X
    # row 8 --> Translation Y
    # row 9 --> Translation Z
    
    bone_anim_data = [[], [], [], [], [], [], [], [], []]
    
    ###############################################################
    # fill bone_anim_data so the rows have the animation length + 1
    for j in range(len(bone_anim_data)):
      for k in range(animation_length):
        bone_anim_data[j].append("")
    
    ###########################################################################
    # check if the bone has 2 or more keyframes (bone has an animation)
    # and store the animation length of that bone (read bone_last_kf_pos,
    # first frame counts on the animation length!, to use later)
    bone_has_anim = False
    anim_length_for_bone = 0 + 1
    for j in range(int(len(bone_last_kf_pos) / 2)):
    #
      if (bone_last_kf_pos[2 * j] == bone.name):
        bone_has_anim = True
        anim_length_for_bone = bone_last_kf_pos[(2 * j) + 1] + 1                    
        break
    #
    
    print("Bone has animation? ", end = "")
    print(bone_has_anim)
    print("Bone animation length: %d" % (anim_length_for_bone))
    
    ###################################################################
    # if export mode is "only keyframes" define current_bone_kf_data
    # and get from it all the bone keyframes in each animation property
    # keyframes on bone_kfs won't necessarily be on order 
    # (do it on bones that have animation)
    current_bone_kf_data = []
    bone_kfs = []
    if (export_mode == "OPT_B" and bone_has_anim == True):
    #
      ##########################################################
      # store the rows of bone_kf_data in which the current bone
      # appears in current_bone_kf_data (to use later)
      current_bone_kf_data = []
      for j in range(len(bone_kf_data)):
        if (bone_kf_data[j][0] == bone.name):
          current_bone_kf_data.append(bone_kf_data[j])
      
      #################################################
      # read current_bone_kf_data to get all the unique
      # keyframe positions of the bone animation
      for j in range(len(current_bone_kf_data)):
      #  
        # store the keyframes found on the first row
        # of current_bone_kf_data in bone_kfs
        if (j == 0):
          for k in range(len(current_bone_kf_data[0])):
            # make k equal to 3
            if (k < 3):
              continue
            bone_kfs.append(current_bone_kf_data[j][k])
          
        # other rows
        for k in range(len(current_bone_kf_data[j])):
        # 
          if (k < 3): # make k equal to 3
            continue
          
          # loop through bone_kfs to check for new keyframe positions
          keyframe_exists = False
          for l in range(len(bone_kfs)):
            if (current_bone_kf_data[j][k] == bone_kfs[l]):
              keyframe_exists = True
              break     
          
          if (keyframe_exists == False):
            bone_kfs.append(current_bone_kf_data[j][k])
        #
      #
      # ~ print(current_bone_kf_data)
      print("Bone's keyframes position:")
      print(bone_kfs)    
    #
    
    print()
    
    # if bone_has_anim equals False only store its first frame animation values
    # if bone_has_anim equals True store (depending on the export mode) its 
    # keyframes/all frame animation values (until the last keyframe)
        
    ########################################
    # loop through the animation of the bone
    # k is used to go through bone_kfs if
    # export mode is "only keyframes"
    k = 0
    for j in range(anim_length_for_bone):
    #      
      ##########################################
      # set scene frame depending on export mode
      if (export_mode == "OPT_B" and bone_has_anim == True and j != 0):
      #
        # check k in case bone_kfs end is reached
        if (k == len(bone_kfs)):
          break
          
        frame = bone_kfs[k]
        scene.frame_set(frame)
        k = k + 1
      #
      else:
      #
        frame = j
        scene.frame_set(frame)
      #
      
      # first bone must be the outermost bone and therefore it 
      # has no parent (batman moment, sorry batman u epik >:])
      if (i == 0):      
        # -90 degree rotation matrix      
        rot_mat = calc_rotation_matrix(math.radians(-90), 0, 0)
        # the first bone has to be rotated -90 degrees 
        # on X to get correct animation values for it
        bone_rel_to_parent_mat = rot_mat * bone.matrix      
      else: # for any other bone
        bone_rel_to_parent_mat = bone.parent.matrix.inverted() * bone.matrix
      
      ##########################################
      # extract bone_rel_to_parent_mat anim data
      
      # bone scaling
      bone_scale = bone_rel_to_parent_mat.to_scale()
      # bone rotation (stored in radians, extrinsic XYZ Euler)
      bone_rotation = bone_rel_to_parent_mat.to_euler("XYZ")
      # bone translation (multiplied by 100 because 1 GU is 100 meters)
      bone_translation = 100 * bone_rel_to_parent_mat.to_translation()
      
      ##########################################################
      # store frame animation values of bone into bone_anim_data
              
      # scaling data
      bone_anim_data[0][frame] = round(bone_scale[0], 2)
      bone_anim_data[1][frame] = round(bone_scale[1], 2)
      bone_anim_data[2][frame] = round(bone_scale[2], 2)
      
      # rotation data (must be in degrees!)
      bone_anim_data[3][frame] = round(math.degrees(bone_rotation[0]), 2)
      bone_anim_data[4][frame] = round(math.degrees(bone_rotation[1]), 2)
      bone_anim_data[5][frame] = round(math.degrees(bone_rotation[2]), 2)  
      
      # position data
      bone_anim_data[6][frame] = round(bone_translation[0], 2)
      bone_anim_data[7][frame] = round(bone_translation[1], 2)
      bone_anim_data[8][frame] = round(bone_translation[2], 2)
      
      # ^ a lot of values can be repeated in each row.
      # When writing the animation data to the CSV is when
      # an optimization method will be done
    #
    
    # ~ for row in bone_anim_data:
      # ~ print(row)
    # ~ print()
    
    ####################################
    # write bone animation data into CSV
    # read bone_anim_data
    ####################################
    for j in range(9):
    #    
      # first 3 rows are scaling data
      # the next 3 rows are rotation data
      # the final 3 rows are translation data        
      # the start of the first row for a bone
      # animation data (Scale X) contains the bone name
      if (j == 0):
        f.write("%s,Linear,Scale X:" % (bone.name))
      elif(j == 1):
        f.write(",Linear,Scale Y:")
      elif(j == 2):
        f.write(",Linear,Scale Z:")
      elif(j == 3):
        f.write(",Linear,Rotation X:")
      elif(j == 4):
        f.write(",Linear,Rotation Y:")
      elif(j == 5):
        f.write(",Linear,Rotation Z:")
      elif(j == 6):
        f.write(",Linear,Translation X:")
      elif(j == 7):
        f.write(",Linear,Translation Y:")
      elif(j == 8):
        f.write(",Linear,Translation Z:")
      
      ###################################
      # write animation row data
      # will print values with 2 decimals
      ###################################
      for k in range(animation_length):
      #                  
        # get current animation value
        current_value = bone_anim_data[j][k]
        
        # write the first value from row
        if (k == 0):
          f.write(",%.2f" % (current_value))
        # compare old_value with current_value. if equal leave blank the
        # animation frame value otherwise write said value. This is done
        # to avoid repeating the same number each time (to save file size)
        elif (old_value == current_value or current_value == ""):          
          f.write(",")        
        else:
          f.write(",%.2f" % (current_value))
        
        # if the end of a row is reached write a newline char to the line
        if (k == (animation_length - 1)):
          f.write("\n")
        
        # store old animation value for the next loop
        if (current_value != ""):
          old_value = current_value
      #
    #
  #
  
  # exporter end
  return {'FINISHED'}


#################################################
# Stuff down is for the menu appending
# of the exporter to work plus some setting stuff
# comes from a Blender importer template
#################################################

from bpy_extras.io_utils import ExportHelper
from bpy.props import StringProperty, BoolProperty, EnumProperty
from bpy.types import Operator

class Export_CSV_BCK(Operator, ExportHelper):
#
  """Save a CSV file for BCK conversion (to use with J3D Animation Editor)"""
  bl_idname = "export_scene.csv_bck"
  bl_label = "Export CSV (for BCK)"  
  filename_ext = ".csv"

  filter_glob = StringProperty(
    default="*.csv",
    options={'HIDDEN'},
    maxlen=255,
      )

  loop_mode = EnumProperty(
    name="Loop Mode",
    description="Choose the loop mode for the animation",
    items=( ('OPT_A', "Once", "Play the animation once and stop at the last frame"),
            ('OPT_B', "Once and Reset", "Play the animation once and stop at the first frame"),
            ('OPT_C', "Loop", "Loop the animation infinitely"),
            ('OPT_D', "Mirrored Once", "Play the animation forwards and then backwards once. Stops at the first frame"),
            ('OPT_E', "Mirrored Loop", "Play the animation forwards and then backwards infinitely")
              ), default='OPT_A'
                )
  export_mode = EnumProperty(
    name="Export Mode",
    description="Choose the method used to export the model animation data",
    items=( ('OPT_A', "All Frames", "Export animation values for each frame of the animation. Slow, higher CSV file size, more accurate animation"),
            ('OPT_B', "Only Keyframes", "Only export the animation keyframe values of each bone. Fast, lower CSV file size, least accurate animation"),
              ), default='OPT_A'
                )
  
  def execute(self, context):
    return write_csv_bck(context, self.filepath, self.loop_mode, self.export_mode)
#

# Only needed if you want to add into a dynamic menu
def menu_export_csv_bck(self, context):
  self.layout.operator(Export_CSV_BCK.bl_idname, text="CSV Animation Table (for BCK) (.csv)")

bpy.utils.register_class(Export_CSV_BCK)
bpy.types.INFO_MT_file_export.append(menu_export_csv_bck)

# test call
bpy.ops.export_scene.csv_bck('INVOKE_DEFAULT')
